﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.Win32;
using FileObject = Sharpmark.QQ.ChatLog.IBaseStorageWrapper.FileObjects.FileObject;

namespace Sharpmark.QQ.ChatLog
{
    public class Manager
    {
        private IStorageWrapper _storage;
        private readonly byte[] _password;
        private readonly string _qqId;

        public List<Chat> ChatList { get; private set; }

        public Manager(string qqId) : this(qqId, null) { }

        public Manager(string qqId, string qqPath)
        {
            ChatList = new List<Chat>();
            _qqId = qqId;
            _storage = new IStorageWrapper(BuildMessageFilePath(qqPath));
            _password = GetGlobalPass();

            InitConversationList();

            ReadConversations();

            _storage = null;
        }

        private string BuildMessageFilePath(string qqPath)
        {
            if (qqPath == null)
            {
                using (RegistryKey reg = Registry.LocalMachine.OpenSubKey(@"Software\Tencent\QQ"))
                {
                    if (reg != null) qqPath = reg.GetValue("Install") as string;
                }
                if (qqPath == null) throw new Exception("Error when try to read QQ path from registry.");
            }
            return qqPath + _qqId + @"\MsgEx.db";
        }

        private void InitConversationList()
        {
            foreach (object type in Enum.GetValues(typeof(ConversationType)))
            {
                ChatList.Add(new Chat { Type = (ConversationType)type });
            }

            foreach (FileObject fileObject in _storage.FoCollection)
            {
                if (fileObject.FileType != 1) continue;

                foreach (Chat chat in ChatList)
                {
                    if (fileObject.FilePath == chat.Type.ToString())
                    {
                        chat.Add(new Conversation { Peer = fileObject.FileName });
                        break;
                    }
                }
            }
        }

        private void ReadConversations()
        {
            Encoding encoding = Encoding.GetEncoding(936);
            foreach (Chat chat in ChatList)
            {
                foreach (Conversation conversation in chat)
                {
                    IList<byte[]> messageList = DecryptMsg(chat.Type + conversation.Peer);

                    foreach (byte[] messageByte in messageList)
                    {
                        var ms = new MemoryStream(messageByte);
                        var br = new BinaryReader(ms, encoding);

                        Message message = new Message
                                              {
                                                  DateTime =
                                                      new DateTime(1970, 1, 1) + new TimeSpan(0, 0, br.ReadInt32()),
                                                  Type = chat.Type.ToString(),
                                                  PeerId = conversation.Peer,
                                                  OwnerId = _qqId,
                                              };
                        switch (chat.Type)
                        {
                            case ConversationType.BimMsg:
                            case ConversationType.C2CMsg:
                            case ConversationType.MobileMsg:
                                ms.Seek(1, SeekOrigin.Current);
                                break;
                            case ConversationType.GroupMsg:
                                ms.Seek(8, SeekOrigin.Current);
                                break;
                            case ConversationType.SysMsg:
                                ms.Seek(4, SeekOrigin.Current);
                                break;
                            case ConversationType.TempSessionMsg: //?
                                ms.Seek(9, SeekOrigin.Current);
                                break;
                        }
                        //NOTE: 获取GroupName, 暂无用处
                        //if (type == MessageType.TempSession)
                        //{
                        //string groupName = encoding.GetString(br.ReadBytes(br.ReadInt32()));
                        //if (groupName.Length > 0) sw.WriteLine("{0}", groupName);
                        //}

                        message.FromUser = encoding.GetString(br.ReadBytes(br.ReadInt32()));
                        string content = encoding.GetString(br.ReadBytes(br.ReadInt32())).Replace(new string(new[] { (char)21 }), "(图片)");
                        content = content.Replace(new string(new[] { (char)20 }), "(表情)");
                        content = content.Replace("\n", Environment.NewLine);
                        //char[] t = msg.ToCharArray();
                        string[] contents = content.Split((char)19);
                        message.Content = contents[0];

                        if (contents.Length > 1) message.FontRaw = contents[1];

                        conversation.Add(message);

                        br.Close();
                        ms.Close();
                    }
                }
            }
        }

        public void Close()
        {
            if (_storage == null)
            {
                return;
            }
            _storage = null;
        }

        private FileObject GetStorageFileObject(string path, string fileName)
        {
            foreach (IBaseStorageWrapper.FileObjects.FileObject fileObject in _storage.FoCollection)
            {
                if (fileObject.CanRead)
                {
                    if (fileObject.FilePath == path && fileObject.FileName == fileName) return fileObject;
                }
            }
            return null;
        }

        private static byte[] Decrypt(byte[] src, byte[] pass, long offset)
        {
            return new QQCrypt().QQDecrypt(src, pass, offset);
        }

        private IList<byte[]> DecryptMsg(string path)
        {
            var msgList = new List<byte[]>();

            int num;
            int[] pos;
            using (FileObject fileObject = GetStorageFileObject(path, "Index.msj"))
            {
                if (fileObject == null) return msgList;
                var fileLen = (int)fileObject.Length;
                num = fileLen / 4;
                pos = new int[num + 1];
                using (BinaryReader br = new BinaryReader(fileObject))
                {
                    for (int i = 0; i < num; ++i)
                    {
                        pos[i] = br.ReadInt32();
                    }
                }
            }
            using (FileObject fileObject = GetStorageFileObject(path, "Data.msj"))
            {
                if (fileObject != null)
                {
                    int fileLen = (int)fileObject.Length;
                    int[] len = new int[num];
                    pos[num] = fileLen;
                    for (int i = 0; i < num; ++i)
                    {
                        len[i] = pos[i + 1] - pos[i];
                    }
                    using (BinaryReader br = new BinaryReader(fileObject))
                    {
                        for (int i = 0; i < num; ++i)
                        {
                            fileObject.Seek(pos[i], SeekOrigin.Begin);
                            byte[] data = br.ReadBytes(len[i]);
                            byte[] msg = Decrypt(data, _password, 0);
                            msgList.Add(msg);
                        }
                    }
                }
            }
            return msgList;
        }

        private byte[] GetGlobalPass()
        {
            System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
            var dataID = new byte[_qqId.Length];
            for (int i = 0; i < _qqId.Length; ++i) dataID[i] = (byte)(_qqId[i]);
            byte[] hashID = md5.ComputeHash(dataID);
            FileObject fileObject = GetStorageFileObject("Matrix", "Matrix.db");
            if (fileObject != null)
            {
                using (var br = new BinaryReader(fileObject))
                {
                    byte[] data = br.ReadBytes((int)fileObject.Length);
                    long len = data.Length;
                    if (len < 6 || data[0] != 0x51 || data[1] != 0x44) return null;
                    if (len >= 32768) return null;

                    int i = 6;
                    while (i < len)
                    {
                        bool bl = false;
                        byte type = data[i++];
                        if (i + 2 > len) break;
                        int len1 = data[i] + data[i + 1] * 256;
                        byte xor1 = (byte)(data[i] ^ data[i + 1]);
                        i += 2;
                        if (i + len1 > len) break;
                        for (int j = 0; j < len1; ++j) data[i + j] = (byte)(~(data[i + j] ^ xor1));
                        if (len1 == 3 && data[i] == 0x43 && data[i + 1] == 0x52 && data[i + 2] == 0x4B)
                        {
                            bl = true;
                        }
                        i += len1;

                        if (type > 7) break;
                        if (i + 4 > len) break;
                        int len2 = data[i] + data[i + 1] * 256 + data[i + 2] * 256 * 256 + data[i + 3] * 256 * 256 * 256;
                        byte xor2 = (byte)(data[i] ^ data[i + 1]);
                        i += 4;
                        if (i + len2 > len) break;
                        if (type == 6 || type == 7)
                        {
                            for (int j = 0; j < len2; ++j) data[i + j] = (byte)(~(data[i + j] ^ xor2));
                        }
                        if (bl && len2 == 0x20)
                        {
                            var dataT = new byte[len2];
                            for (int j = 0; j < len2; ++j) dataT[j] = data[i + j];
                            return Decrypt(dataT, hashID, 0);
                        }
                        i += len2;
                    }
                    if (i != len) throw new Exception("password is empty.");
                }
            }
            throw new Exception("password is empty.");
        }

        /*
        private void OutputMessagesToFile()
        {
            // NOTE: this method is for test only. later will use WCF instead.
            foreach (Conversation conversation in _conversationList)
            {
                //Console.WriteLine("Conversation {0} has {1} sessions.", conversation.Type, conversation.Count);
                string filePath = conversation.Type + "\\";
                Directory.CreateDirectory(filePath);

                foreach (Session session in conversation)
                {
                    //Console.WriteLine("Session to {0} has {1} messages.", session.Peer, session.Count);
                    string fileName = filePath + session.Peer + ".txt";
                    using (var fs = new FileStream(fileName, FileMode.Create))
                    {
                        using (var sw = new StreamWriter(fs))
                        {
                            foreach (Message message in session)
                            {
                            }
                            sw.Close();
                        }
                        fs.Close();
                    }
                }
            }
        }
        */

    }

}
